<?php
/**
 * This file is part of the wangningkai/php-redis-cli.
 * (c) wangningkai <i@ningkai.wang>
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace App;

use Inhere\Console\Command;
use Inhere\Console\IO\Input;
use Inhere\Console\IO\Output;
use Redis;
use RedisException;

class AppCommand extends Command
{
    /**
     * @var string
     */
    protected static $name = 'connect';

    /**
     * @var string
     */
    protected static $description = 'Connect the redis-server';

    /** @var \Redis */
    protected $redis = false;
    /**
     * @var
     */
    protected $host;
    /**
     * @var
     */
    protected $port;
    /**
     * @var
     */
    protected $auth;
    /**
     * @var string
     */
    protected $db = '-';
    /**
     * @var string
     */
    protected $version = '';
    /**
     * @var int
     */
    protected $pageNumber = 50;
    /**
     * @var array
     */
    protected $keys = [];

    /**
     * @usage redis-tool.phar --host [hostname] --port [port] --password [password] database=[database]
     * @arguments
     *  database     Database number (default: 0).
     *
     * @options
     *
     *  --host    Server hostname (default: 127.0.0.1).
     *  --port    Server port (default: 6379).
     *  --auth    Password to use when connecting to the server.
     *  --db      Database number (default: 0).
     *
     * @param Input $input
     * @param Output $output
     * @return int|mixed|void
     */
    public function execute($input, $output)
    {
        $args = $input->getArgs();
        $opts = $input->getOpts();
        $this->connect($args, $opts);
        do {
            $host = $this->host;
            $port = $this->port;
            $auth = $this->auth;
            $db = $this->db;
            $command = trim($this->read("{$host}:{$port}[$db]"));
            try {
                $this->redis->ping();
            } catch (\Exception $exception) {
                $this->redis->connect($host, $port);
                $this->redis->auth($auth);
            }
            switch (true) {
                case stripos($command, 'info') === 0:
                    $parameter = trim(substr($command, 4));
                    $this->getInfo($parameter);
                    break;
                case stripos($command, 'config') === 0:
                    $parameter = trim(substr($command, 7));
                    $this->getConfig($parameter);
                    break;
                case stripos($command, 'select ') === 0:
                    $parameter = trim(substr($command, 7));
                    $this->selectDb($parameter);
                    break;
                case stripos($command, 'ls') === 0:
                    $parameter = trim(substr($command, 2));
                    // 先写这里,回头再抽象
                    $this->listTable($parameter);
                    break;
                case stripos($command, 'set ') === 0:
                    $parameter = trim(substr($command, 4));
                    $parameter = explode(' ', $parameter, 2);
                    $this->set($parameter);
                    break;
                case stripos($command, 'get ') === 0:
                    $parameter = trim(substr($command, 4));
                    $parameter = explode(' ', $parameter, 2);
                    $this->get($parameter);
                    break;
                case stripos($command, 'ttl') === 0:
                    $parameter = trim(substr($command, 3));
                    $parameter = explode(' ', $parameter, 2);
                    if (count($parameter) === 1) {
                        $this->getTtl($parameter);
                    } else {
                        $this->setTtl($parameter);
                    }
                    break;
                case stripos($command, 'persist ') === 0:
                    $parameter = trim(substr($command, 8));
                    $this->persist($parameter);
                    break;
                case stripos($command, 'raw ') === 0:
                    $parameters = trim(substr($command, 4));
                    $this->rawCommand($parameters);
                    break;
                case stripos($command, 'exit') === 0:
                    $this->close();
                    $output->success('Bye!', true);
                    break;
                case stripos($command, 'help') === 0:
                default:
                    // 帮助列表
                    $this->output->aList([
                        'help : 显示可用命令',
                        'select < 0 > : 切换数据库,默认0 ',
                        'raw: 原生redis命令',
                        'ls : 列出所有keys',
                        'ls h?llo : 列出匹配keys,?通配1个字符,*通配任意长度字符,[aei]通配选线,特殊符号用\隔开',
                        'ttl key [ttl second] : 获取/设定生存时间,传第二个参数会设置生存时间',
                        'persist key : 移除给定key的生存时间',
                        'mv key new_key : key改名,如果新名字存在则会报错',
                        'rm key : 刪除key,支持通配符匹配',
                        'get key : 获取值',
                        'set key : 设置值',
                        'config  [dir]: 获取配置,可选参数[配置名称(支持通配符)]',
                        'info: 获取当前Redis服务器信息'
                    ], 'Help List 命令列表');
                    break;
            }
        } while (strtolower($command) !== 'exit');
        $this->redis->close();
        $output->success('Bye!', true);
    }

    /**
     * Connect redis server
     *
     * @param $args
     * @param $opts
     * @return bool
     */
    protected function connect($args, $opts): bool
    {
        $this->redis = new Redis();
        if (!empty($opts)) {
            $this->host = $host = data_get($opts, 'host', '127.0.0.1');
            $this->port = $port = data_get($opts, 'port', '6379');
            $this->auth = $auth = data_get($opts, 'auth', '~');
            try {
                $this->redis->connect($host, $port);
            } catch (RedisException $e) {
                $this->output->error("Failed to connect {$host}:{$port} ! [MESSAGE] " . $e->getMessage(), true);
            }
            $this->redis->auth($auth);
        } else {
            do {
                $this->host = $host = $this->ask('Redis Host', '127.0.0.1');
                $this->port = $port = $this->ask('Redis Port', '6379');
                $this->auth = $auth = $this->ask('Redis auth', '~');
                try {
                    $connResult = $this->redis->connect($host, $port);
                } catch (RedisException $e) {
                    $connResult = false;
                    $this->output->error("Failed to connect {$host}:{$port} ! [MESSAGE] " . $e->getMessage());
                }
            } while ($connResult !== true);
        }
        $this->output->success("Connect to {$host}:{$port} successfully!");
        $this->db = data_get($args, 'database', 0);
        $this->version = $this->redis->info('redis_version');
        return true;
    }

    /**
     * Select database
     * @param $parameter
     */
    protected function selectDb($parameter): void
    {
        $result = $this->redis->select($parameter);
        if ($result === true) {
            $this->db = $parameter;
            $this->output->success('select database successfully!');
        } else {
            $this->output->error('Failed to select database!');
        }
    }

    /**
     * Show config
     * @param $parameter
     */
    protected function getConfig($parameter): void
    {
        if (empty($parameter)) {
            $parameter = '*';
        }
        try {
            $config = $this->redis->config('GET', $parameter);
            if (!is_array($config)) {
                $config = array_wrap($config);
            }
            $this->output->aList($config, 'CONF:');
        } catch (\Exception $e) {
            $this->output->error('Failed to execute command! [MESSAGE] ' . $e->getMessage());
        }
    }

    /**
     * Show info
     * @param $parameter
     */
    protected function getInfo($parameter): void
    {
        /*  @var array $info */
        $info = $this->redis->info();
        if ($parameter) {
            $value = data_get($info, $parameter, '');
            $info = [
                $parameter => $value
            ];
        }
        $this->output->aList($info, 'INFO:');
    }

    /**
     * 获取列表和key对应的类型,并返回表格
     * @param string $search
     * @return bool
     */
    protected function listTable($search = ''): ?bool
    {
        if (empty($search)) {
            $search = '*';
        }

        // 换个方式,大于2.8.0那么使用Scan搜索
        if (version_compare($this->version, '2.8.0')) {
            $iterator = null;
            while ($keys = $this->redis->scan($iterator, $search, $this->pageNumber)) {
                $data = [];
                foreach ($keys as $key) {
                    $type = $this->redis->type($key);
                    if ($type === 0) {
                        continue;//不存在那么就直接跳过
                    }
                    // 根据类型显示颜色
                    $type = $this->transType($type);
                    $data[$key] = [$type, $key];
                    $this->keys[] = $key;
                }
                $data[0] = ['TYPE', 'KEY'];
                $this->output->table($data);
                $this->keys = array_unique($this->keys);
                // 最后一页不用了.
                if (count($data) >= $this->pageNumber) {
                    $isBreak = $this->output->confirm('回车继续...');
                    if (!$isBreak) {
                        return true;
                    }
                }
                $this->keys = array_unique($this->keys);
            }
        } else {
            $keys = $this->redis->keys($search);
            sort($keys);
            $data = [];
            $data[0] = ['TYPE', 'KEY'];
            // 加一个分页功能
            foreach ($keys as $row => $key) {
                $type = $this->redis->type($key);
                if ($type === 0) {
                    continue;//不存在那么就直接跳过
                }
                // 根据类型显示颜色
                $type = $this->transType($type);
                $data[$key] = [$type, $key];
                $this->keys[] = $key;
                if ($row !== 0 && $row % $this->pageNumber === 0) {
                    $this->output->table($data);
                    $isBreak = $this->output->confirm('回车继续...');
                    if (!$isBreak) {
                        $this->keys = array_unique($this->keys);
                        return true;
                    }
                    $data = [];
                }
            }
            $this->output->table(
                ['TYPE', 'KEY'],
                $data
            );
            $this->keys = array_unique($this->keys);
        }
    }

    /**
     * 重命名key
     * @param $parameters
     */
    protected function rename($parameters): void
    {
        try {
            if (!array_key_exists('1', $parameters)) {
                throw new \Exception('缺少第2个参数');
            }
            $result = $this->redis->renameNx($parameters[0], $parameters[1]);
            // 格式化显示
            if ($result === true) {
                // 成功
                $this->output->success('修改成功');
            } else {
                // 失败
                $this->output->error('修改失败');
            }
        } catch (\Exception $e) {
            $this->output->error($e->getMessage());
        }
    }

    /**
     * 删除key
     * @param $parameters
     */
    protected function rm($parameters): void
    {
        try {
            // 支持 patten批量删除
            $removeKeys = [];
            while ($keys = $this->redis->scan($iterator, $parameters[0], $this->pageNumber)) {
                $removeKeys = array_merge($removeKeys, $keys);
            }
            if (empty($removeKeys)) {
                throw new \Exception("KEY: {$parameters[0]} 不存在");
            }
            $count = count($removeKeys);
            $confirm = $this->output->confirm("确定要删除 {$parameters[0]} 共{$count}条记录 ?", false);
            if ($confirm) {
                $this->redis->del($removeKeys);
                $this->output->success('删除成功');
            }
        } catch (\Exception $e) {
            $this->output->error($e->getMessage());
        }
    }

    /**
     * 设置过期时间为永久
     * @param $key
     * @return bool
     */
    protected function persist($key): bool
    {
        $this->redis->persist($key);
        $this->output->success("{$key} 设置过期时间为永久.");
        return true;
    }

    /**
     * 从 0,1,2..这种类型,转换成显示类型
     * @param $type
     * @return string
     */
    protected function transType($type): string
    {
        switch ($type) {
            case 1:
                $type = 'STRING';
                break;
            case 2:
                $type = 'SET';
                break;
            case 3:
                $type = 'LIST';
                break;
            case 4:
                $type = 'ZSET';
                break;
            case 5:
                $type = 'HASH';
                break;
        }
        return $type;
    }

    /**
     * 转换为方便输出的ttl格式
     * @param $ttl
     * @return string
     */
    protected function transTtl($ttl): string
    {
        switch ($ttl) {
            case -2:
                $ttl = 'KEY不存在';
                break;
            case -1:
                $ttl = '永久';
                break;
            default:
        }
        return $ttl;
    }

    /**
     * 获取并显示数据的ttl生存时间
     * @param $parameters
     */
    protected function getTtl($parameters): void
    {
        try {
            $ttl = $this->redis->ttl($parameters[0]);
            // 格式化显示
            $ttl = $this->transTtl($ttl);
            $this->output->table([
                ['KEY', 'TTL (秒s)'],
                [$parameters[0], $ttl],
            ]);
        } catch (\Exception $e) {
            $this->output->error($e->getMessage());
        }
    }

    /**
     * @param $parameters
     */
    protected function setTtl($parameters): void
    {
        try {
            $result = $this->redis->expire($parameters[0], (integer)$parameters[1]);
            // 格式化显示
            $result = $result === true ? ('生存时间设置为: ' . (integer)$parameters[1] . ' (秒)') : '失败';
            $this->output->table([
                ['KEY', 'TTL (秒s)'],
                [$parameters[0], $result],
            ]);
        } catch (\Exception $e) {
            $this->output->error($e->getMessage());
        }
    }

    /**
     * @param $parameters
     * @return bool
     */
    protected function set($parameters): ?bool
    {
        try {
            $key = $parameters[0];
            if ($this->redis->exists($key)) {
                $confirm = $this->output->confirm("KEY: {$key} 已存在,确定覆盖?");
                if (!$confirm) {
                    return true;
                }
                // 这里显示下之前旧值,方便修改
                $this->get($parameters);
                $type = $this->redis->type($key);
                // 从整型值,转换为下面兼容的类型.
                switch ($type) {
                    case 1:
                        $type = 'String';
                        break;
                    case 2:
                        $type = 'Set';
                        break;
                    case 3:
                        $type = 'List';
                        break;
                    case 4:
                        $type = 'ZSet';
                        break;
                    case 5:
                        $type = 'Hash';
                        break;
                }
            } else {
                // 优化下这个逻辑,虽然修改已有数据类型是允许的,但是为了方便使用,我这里设置成不能改
                $type = $this->output->select('请选择数据类型', ['String', 'Hash', 'List', 'Set', 'ZSet']);
                $type = strip_tags($type);
            }
            // 处理不同类型数据
            switch ($type) {
                case 'String':
                    $this->setString($key);
                    break;
                case 'Hash':
                    $this->setHash($key);
                    break;
                case 'List':
                    $this->setList($key);
                    break;
                case 'Set':
                    $this->setSet($key);
                    break;
                case 'ZSet':
                    $this->setZSet($key);
                    break;
            }
        } catch (\Exception $e) {
            $this->output->error($e->getMessage());
        }
    }

    /**
     * @param $key
     */
    protected function setString($key): void
    {
        $value = $this->output->ask('请输入值', null, static function ($value) {
            if (empty($value)) {
                throw new \RuntimeException('不能为空');
            }
            return $value;
        });
        $this->redis->set($key, $value);
        $this->output->success('设置成功!');
        $this->get([$key]);
    }

    /**
     * @param $key
     * @return bool
     */
    protected function setZSet($key): ?bool
    {
        do {
            $exit = false;
            $item_key = $this->output->ask('编辑: help查看功能,exit编辑完成退出', 'ZSet: ' . $key);
            $item_key = trim($item_key);
            switch (true) {
                case stripos($item_key, 'exit') === 0:
                    $exit = true;
                    $this->get([$key]);
                    break;
                // TODO: rm 和 add 操作似乎不统一? 还没想好用哪种,各有优缺点.
                case stripos($item_key, 'rm ') === 0:
                    $parameter = trim(substr($item_key, 3));
                    $this->redis->zRem($key, $parameter);
                    $this->output->success('删除成功!');
                    $this->get([$key]);
                    break;
                case stripos($item_key, 'add') === 0:
                    $item_key = $this->output->ask('请输入排序权重值:Score', null, static function ($value) {
                        if (empty($value)) {
                            throw new \RuntimeException('不能为空');
                        }
                        if (!is_numeric($value)) {
                            throw new \RuntimeException('必须为数字');
                        }
                        return $value;
                    });
                    $item_value = $this->output->ask('请输入Member值', null, static function ($value) {
                        if (empty($value)) {
                            throw new \RuntimeException('不能为空');
                        }
                        return $value;
                    });
                    $this->redis->zAdd($key, (int)$item_key, $item_value);
                    $this->output->success('修改成功!');
                    $this->get([$key]);
                    break;
                case stripos($item_key, 'help ') === 0:
                default:
                    $this->output->aList([
                        'help : 显示可用命令',
                        'add  : 增加记录',
                        'rm <value> : 移除 value',
                        'exit : 退出编辑',
                    ], 'Zset 命令列表');
            }
        } while ($exit !== true);
        return true;
    }

    /**
     * @param $key
     * @return bool
     */
    protected function setSet($key): ?bool
    {
        do {
            $exit = false;
            $item_key = $this->output->ask('编辑: help查看功能,exit编辑完成退出', 'Set: ' . $key);
            $item_key = trim($item_key);
            switch (true) {
                case stripos($item_key, 'exit') === 0:
                    $exit = true;
                    $this->get([$key]);
                    break;
                // TODO: rm 和 add 操作似乎不统一? 还没想好用哪种,各有优缺点.
                case stripos($item_key, 'rm ') === 0:
                    $parameter = trim(substr($item_key, 3));
                    $this->redis->sRem($key, $parameter);
                    $this->output->success('删除成功!');
                    $this->get([$key]);
                    break;
                case stripos($item_key, 'add') === 0:
                    $item_value = $this->output->ask('请输入value', null, static function ($value) {
                        if (empty($value)) {
                            throw new \RuntimeException('不能为空');
                        }
                        return $value;
                    });
                    $this->redis->sAdd($key, $item_value);
                    $this->output->success('修改成功!');
                    $this->get([$key]);
                    break;
                case stripos($item_key, 'help ') === 0:
                default:
                    $this->output->title('Set 命令列表');
                    $this->output->aList([
                        'help : 显示可用命令',
                        'add  : 增加记录',
                        'rm <value> : 移除 value',
                        'exit : 退出编辑',
                    ]);
            }
        } while ($exit != true);
        return true;
    }

    /**
     * @param $key
     * @return bool
     */
    protected function setList($key): ?bool
    {
        do {
            $exit = false;
            $item_key = $this->output->ask('编辑: help查看功能,exit编辑完成退出', 'List: ' . $key);
            $item_key = trim($item_key);
            switch (true) {
                case stripos($item_key, 'exit') === 0:
                    $exit = true;
                    $this->get([$key]);
                    break;
                case stripos($item_key, 'lpop') === 0:
                    $v = $this->redis->lPop($key);
                    $this->output->success("值 [$v] 出队");
                    $this->get([$key]);
                    break;
                case stripos($item_key, 'rpop') === 0:
                    $v = $this->redis->rPop($key);
                    $this->output->success("值 [$v] 出队");
                    $this->get([$key]);
                    break;
                case stripos($item_key, 'lpush') === 0:
                    $item_value = $this->output->ask('请输入value', null, static function ($value) {
                        if ($value === null) {
                            throw new \RuntimeException('不能为空');
                        }
                        return $value;
                    });
                    $item_value = trim($item_value);
                    $this->redis->lPush($key, $item_value);
                    $this->output->success("值 [$item_value] 左入队");
                    $this->get([$key]);
                    break;
                case stripos($item_key, 'rpush') === 0:
                    $item_value = $this->output->ask('请输入value', null, static function ($value) {
                        if ($value === null) {
                            throw new \RuntimeException('不能为空');
                        }
                        return $value;
                    });
                    $item_value = trim($item_value);
                    $this->redis->rPush($key, $item_value);
                    $this->output->success("值 [$item_value] 右入队");
                    $this->get([$key]);
                    break;
                case stripos($item_key, 'help ') === 0:
                default:
                    $this->output->aList([
                        'help  : 显示可用命令',
                        'lpush : 左入队',
                        'rpush : 右入队',
                        'lpop  : 左出队',
                        'rpop  : 右出队',
                        'exit  : 退出编辑',
                    ], 'List 命令列表');
            }
        } while ($exit !== true);
        return true;
    }

    /**
     * 设置 hash数据
     * @param $key
     * @return bool
     */
    protected function setHash($key): ?bool
    {
        do {
            $exit = false;
            $item_key = $this->output->ask('编辑: help查看功能,exit编辑完成退出', 'Hash: ' . $key);
            $item_key = trim($item_key);
            switch (true) {
                case stripos($item_key, 'exit') === 0:
                    $exit = true;
                    $this->get([$key]);
                    break;
                case stripos($item_key, 'rm ') === 0:
                    $parameter = trim(substr($item_key, 3));
                    $this->redis->hDel($key, $parameter);
                    $this->output->success('删除成功!');
                    $this->get([$key]);
                    break;
                case stripos($item_key, 'mv ') === 0:
                    $parameter = trim(substr($item_key, 3));
                    $parameter = explode(' ', $parameter, 2);
                    if (count($parameter) !== 2) {
                        throw new \RuntimeException('缺少参数');
                    }
                    $v = $this->redis->hGet($key, $parameter[0]);
                    // 改名操作 - 是否用事务?
                    $this->redis->multi();
                    $this->redis->hSet($key, $parameter[1], $v);
                    $this->redis->hDel($key, $parameter[0]);
                    $this->redis->exec();
                    $this->output->success('修改成功!');
                    $this->get([$key]);
                    break;
                case stripos($item_key, 'set') === 0:
                    $item_key = $this->output->ask('请输入key', null, static function ($value) {
                        if (empty($value)) {
                            throw new \RuntimeException('不能为空');
                        }
                        return $value;
                    });
                    $item_value = $this->output->ask('请输入value', null, static function ($value) {
                        if (empty($value)) {
                            throw new \RuntimeException('不能为空');
                        }
                        return $value;
                    });
                    $this->redis->hSet($key, $item_key, $item_value);
                    $this->output->success("修改成功!");
                    $this->get([$key]);
                    break;
                case stripos($item_key, 'help ') === 0:
                default:
                    $this->output->aList([
                        'help : 显示可用命令',
                        'set  : 增加记录',
                        'rm <key> : 移除key',
                        'mv <key> <key_new> : key改名',
                    ], 'Hash 命令列表');
            }
        } while ($exit !== true);
        return true;
    }

    /**
     * 获取key数据详细内容
     * @param $parameters
     */
    protected function get($parameters): void
    {
        try {
            if (!$this->redis->exists($parameters[0])) {
                throw new \Exception("KEY: {$parameters[0]} 不存在");
            }
            $key = $parameters[0];
            // 获取类型
            $type = $this->redis->type($key);
            $typeStr = $this->transType($type);
            // 获取ttl
            $ttl = $this->redis->ttl($key);
            $ttlStr = $this->transTtl($ttl);
            // 输出
            $this->output->table(
                [
                    ['TYPE', 'KEY', 'TTL'],
                    [$typeStr, $key, $ttlStr],
                ],
                'STATUS:'
            );
            // 根据类型显示值
            // none(key不存在) int(0)
            // string(字符串) int(1)
            // list(列表) int(3)
            // set(集合) int(2)
            // zset(有序集) int(4)
            // hash(哈希表) int(5)
            switch ($type) {
                case 0:
                    throw new \Exception("KEY: {$key} 不存在");
                    break;
                case 1:
                    $content = (string)$this->redis->get($key);
                    $this->output->section('VALUE:', $content);
                    // 清理下数据
                    unset($content);
                    break;
                case 2:
                    // 集合set
                    $value = $this->redis->sMembers($key);
                    $this->output->aList($value, 'VALUE:');
                    break;
                case 3:
                    // 列表List
                    $value = $this->redis->lRange($key, 0, -1);
                    $this->output->aList($value, 'VALUE:');
                    break;
                case 4:
                    // 有续集Zset
                    $value = $this->redis->zRevRange($key, 0, -1);//按照score倒序拍
                    $data[0] = ['ID', 'SCORE', 'MEMBER'];
                    foreach ($value as $id => $item) {
                        $data[] = [
                            $id,
                            $this->redis->zScore($key, $item),
                            $item,
                        ];
                    }
                    $this->output->table($data, 'VALUE:');
                    break;
                case 5:
                    // 哈希表
                    $value = (array)$this->redis->hGetAll($key);
                    $data[0] = ['KEY', 'MEMBER'];
                    foreach ($value as $key => $item) {
                        $data[] = [
                            $key, $item,
                        ];
                    }
                    $this->output->table($data, 'VALUE:');
                    break;
            }
        } catch (\Exception $e) {
            $this->output->error($e->getMessage());
        }
    }

    /**
     * execute raw command
     *
     * @param $parameter
     */
    protected function rawCommand($parameter): void
    {
        $args = explode(' ', $parameter);
        $name = array_shift($args);
        $params = $args;
        $result = $this->executeRawCommand($name, $params);
        if (!$result) {
            $this->output->error('Failed to execute command!');
        } else {
            if (!is_array($result)) {
                $result = array_wrap($result);
            }
            $this->output->aList($result, 'RAW:');
        }
    }

    /**
     * Close connection
     */
    protected function close(): void
    {
        if (!$this->redis) {
            try {
                $this->redis->close();
            } catch (\RedisException $e) {
                $message = "Failed to close redis DB connection : {$e->getMessage()}";
                $this->output->error($message);
            }
        }
    }

    /**
     * Execute Command
     *
     * @param $name
     * @param array $params
     * @return mixed
     */
    protected function executeCommand($name, $params = [])
    {
        try {
            $result = call_user_func_array([$this->redis, $name], $params);
            return $this->parseResponse($result);
        } catch (\RedisException $e) {
            $message = "Failed to execute Redis command ($name): {$e->getMessage()}";
            $this->output->error($message);
        }
    }

    /**
     * Execute Command
     *
     * @param $name
     * @param array $params
     * @return mixed
     */
    protected function executeRawCommand($name, $params = [])
    {
        $raw = [];
        foreach ($this->parseRequest($params) as $item) {
            $raw[] = $item;
        }
        try {
            $result = call_user_func_array([$this->redis, 'rawCommand'], array_merge(explode(' ', $name), $raw));
            return $this->parseResponse($result);
        } catch (\RedisException $e) {
            $message = "Failed to execute Redis command ($name): {$e->getMessage()}";
            $this->output->error($message);
        }
    }

    /**
     * Parse raw request
     *
     * @param $params
     * @return \Generator
     */
    protected function parseRequest($params): ?\Generator
    {
        foreach ($params as $key => $value) {
            if (is_array($value)) {
                foreach ($this->parseRequest($value) as $item) {
                    yield $item;
                }
            } elseif ($value === null) {
                continue;
            } elseif (is_int($key)) {
                yield $value;
            } else {
                yield $key;
                yield $value;
            }
        }
    }

    /**
     * Parse raw response
     *
     * @param string $result
     * @return mixed
     */
    protected function parseResponse($result)
    {
        if ($result === 'PONG' || $result === 'OK') {
            return true;
        }
        if ($result === false) {
            return false;
        }
        return $result;
    }
}
